package junior.db.dao.query;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import junior.db.dao.base.AbstractFilter;
import junior.db.dao.base.Expression;
import junior.db.dao.base.JoinQuery;
import junior.db.dao.base.Limit;
import junior.db.dao.base.MybatisHelper;
import junior.db.dao.base.Sort;
import junior.db.dao.base.SubQuery;
import junior.db.dao.base.UnionQuery;
import junior.db.dao.base.IFilterItem;
import junior.db.dao.mybatis.type.ResultType;
import junior.db.query.QueryHelper;
import junior.util.string.Strings;
import lombok.EqualsAndHashCode;
import org.springframework.dao.InvalidDataAccessResourceUsageException;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;

/**
 * AbstractFilter的子类，跟父类相比，多了排序、查询字段相关的distinct和别名
 */
public abstract class AbstractQuery<Q> extends AbstractFilter<Q> {
    protected Set<String> ignoreSelectFields = Sets.newHashSet();
    protected List<ISelectItem> selects = Lists.newArrayList();
    private Map<String, String> aliasMap;
    
    private List<Sort> sorts;
    private boolean selectAll;
    private boolean selectAsAlias;
    protected boolean distinct;
    private String customFirst;
    private String customLast;
    private Limit limit;
    
    protected ResultType<?> resultType;
    
    protected List<JoinQuery> joinList;
    protected UnionQuery unionQuery;
    
    private boolean selectFieldSettled;
    
    public AbstractQuery() {}
    
    public AbstractQuery(boolean immutable) {
        super(immutable);
    }
    
    public boolean isSelectAll() {
        return selectAll;
    }
    
    public Q distinct() {
        return distinct(true);
    }
    
    public Q distinct(boolean distinct) {
        this.distinct = distinct;
        return (Q) this;
    }
    
    public boolean isSelectDistinct() {
        return distinct;
    }
    
    public String getDistinct() {
        return distinct ? "distinct" : "";
    }
    
    public Q withAlias(String alias) {
        super.withPrefix(alias);
        return (Q) this;
    }
    
    public String getCustomFirst() {
        return customFirst;
    }
    
    public Q withCustomFirst(String customFirst) {
        this.customFirst = customFirst;
        return (Q) this;
    }
    
    public String getCustomLast() {
        return customLast;
    }
    
    public Q withCustomLast(String customLast) {
        this.customLast = customLast;
        return (Q) this;
    }
    public Limit getLimit() {
        return limit;
    }
    public Q limit(int limit) {
        this.limit = new Limit(limit);
        return (Q) this;
    }
    public Q limit(int start, int limit) {
        this.limit = new Limit(start, limit);
        return (Q) this;
    }
    
    @Override
    public List<IFilterItem> getFilters() {
        return super.getFilterItems(true);
    }
    
    public List<String> getGroupBies() {
        List<String> sqls = super.getGroupBies();
        List<JoinQuery> joinParams = getJoinList();
        if (joinParams != null) {
            for (JoinQuery joinParam : joinParams) {
                if (joinParam.getJoinQuery() instanceof SubQuery) {
                    continue;
                }
                List<String> joinGroupBies = joinParam.getJoinQuery().getGroupBies();
                if (joinGroupBies != null) {
                    if (sqls == null) {
                        sqls = new ArrayList<>();
                    }
                    sqls.addAll(joinGroupBies);
                }
            }
        }
        return sqls != null && sqls.isEmpty() ? null : sqls;
    }
    
    public List<String> getHavings() {
        List<String> sqls = super.getHavings();
        
        List<JoinQuery> joinParams = getJoinList();
        if (joinParams != null) {
            for (JoinQuery joinParam : joinParams) {
                if (joinParam.getJoinQuery() instanceof SubQuery) {
                    continue;
                }
                List<String> joinHavings = joinParam.getJoinQuery().getHavings();
                if (joinHavings != null) {
                    if (sqls == null) {
                        sqls = new ArrayList<>();
                    }
                    sqls.addAll(joinHavings);
                }
            }
        }
        
        return sqls != null && sqls.isEmpty() ? null : sqls;
    }
    
    /**
     * 是否有select字段，此处不包括表达式，如果selectAll为true（调用过selectAll()方法），则返回false
     * @return
     */
    public boolean hasAnySelectFields() {
        return existAnySelectFields() || !ignoreSelectFields.isEmpty() || selectAll;
    }
    /**
     * 是否有select字段，此处不包括表达式，忽略selectAll
     * @return
     */
    public boolean hasAnySelectFieldsWithExceptSelectAll() {
        return existAnySelectFields() || !ignoreSelectFields.isEmpty();
    }
    
    /**
     * 是否有select（包括字段和表达式），如果selectAll为true（调用过selectAll()方法），则返回false
     * @return
     */
    public boolean hasAnySelects() {
        return !this.selects.isEmpty() || !ignoreSelectFields.isEmpty() || selectAll;
    }
    /**
     * 是否有select（包括字段和表达式），忽略selectAll
     * @return
     */
    public boolean hasAnySelectsWithExceptSelectAll() {
        return !this.selects.isEmpty() || !ignoreSelectFields.isEmpty();
    }
    
    /**
     * 查询所有字段
     * @return
     */
    public Q selectAll() {
        selectAll = true;
        return (Q) this;
    }
    
    /**
     * 查询所有字段，除了指定的字段
     * @param excludeFields 指定不查询的字段
     * @return
     */
    public Q selectAllWithExcludes(String... excludeFields) {
        selectAll = true;
        this.excludeSelectFields(excludeFields);
        return (Q) this;
    }
    
    /**
     * <h3>同时指定多个查询字段</h3>
     * @param fields
     * @return
     */
    public Q selects(String... fields) {
        if (fields != null) {
            for (String f : fields) {
                select(f);
            }
        }
        return (Q) this;
    }
    
    /**
     * <h3>指定查询字段</h3>
     * <p>field可以为一个函数，格式为：function('${field}')，如</p>
     * <p>select(sum('${orderAmount}'))</p>
     * <p>select(ifnull('${updateTime}', '${createTime}'))</p>
     * @param field 字段名
     * @return
     */
    public Q select(String field) {
        return select(field, aliasMap != null ? aliasMap.get(field) : null);
    }
    
    /**
     * <h3>指定查询字段，同时指定别名</h3>
     * <p>field可以为一个函数，格式为：function(${field})，如</p>
     * <p>select("sum(${orderAmount})", "orderAmountCount")</p>
     * <p>select("ifnull(${updateTime}, ${createTime}", "time"))</p>
     * @param field 字段名
     * @param alias 别名
     * @return
     */
    public Q select(String field, String alias) {
        SelectField select = new SelectField(field, alias);
        if (! this.selects.stream().filter(item -> item.getType() == SelectItemType.Field).anyMatch(item -> item.equals(select))) {
            selects.add(select);
        }
        return (Q) this;
    }
    
    /**
     * select一个表达式，如: select 1, select 'test'
     * @param expression
     * @return
     */
    public Q selectExpression(String expression) {
        selects.add(new SelectExpression(expression, null, null));
        return (Q) this;
    }
    
    /**
     * select一个表达式，如: select 1, select 'test'
     * @param expression
     * @param alias
     * @return
     */
    public Q selectExpression(String expression, String alias) {
        selects.add(new SelectExpression(expression, alias, null));
        return (Q) this;
    }
    
    public Q selectExpression(String expression, String alias, Map<String, Object> argsValues) {
        selects.add(new SelectExpression(expression, alias, argsValues));
        return (Q) this;
    }
    
    /**
     * <h3>根据类型指定查询字段</h3>
     * <p>类型中的查询字段规则：</p>
     * <p>包含类型：</p>
     * <ul>
     *     <li>8个基本类型及其封装类型</li>
     *     <li>String, Date, Calendar</li>
     *     <li>BigDecimal, BigInteger</li>
     * </ul>
     * <p>不包含类型（优先级高于包含类型）：</p>
     * <ul>
     *     <li>不在上面包含类型中的，如数组，集合，自定义类型等</li>
     *     <li>字段或getter()上有@junior.db.annotation.Transient注解</li>
     *     <li>字段或getter()上有@javax.persistence.Transient注解</li>
     *     <li>字段的修饰符有transient</li>
     * </ul>
     * @param entityClass
     * @return
     */
    public Q selectEntity(Class<?> entityClass) {
        return selects(QueryHelper.getDBFieldNames(entityClass, null));
    }
    
    /**
     * <h3>根据类型指定查询字段，同时指定排除的字段</h3>
     * <p>类型中的查询字段规则：</p>
     * <p>包含类型：</p>
     * <ul>
     *     <li>8个基本类型及其封装类型</li>
     *     <li>String, Date, Calendar</li>
     *     <li>BigDecimal, BigInteger</li>
     * </ul>
     * <p>不包含类型（优先级高于包含类型）：</p>
     * <ul>
     *     <li>不在上面包含类型中的，如数组，集合，自定义类型等</li>
     *     <li>excludeFields指定的字段</li>
     *     <li>字段或getter()上有@junior.db.annotation.Transient注解</li>
     *     <li>字段或getter()上有@javax.persistence.Transient注解</li>
     *     <li>字段的修饰符有transient</li>
     * </ul>
     * @param entityClass
     * @param excludeFields
     * @return
     */
    public Q selectEntityWithExcludes(Class<?> entityClass, String... excludeFields) {
        return selects(QueryHelper.getDBFieldNames(entityClass, null, excludeFields));
    }
    
    /**
     * 仅指定别名，如果单调用此方法，field指定的字段将不会出现了查询字段列表中，一般是配合selects(fields)方法使用
     * @param field
     * @param alias
     * @return
     */
    public Q alias(String field, String alias) {
        Optional<ISelectItem> optional = this.selects.stream().filter(item -> item.getType() == SelectItemType.Field && field.equals(item.getSelect()))
        .findFirst();
        if (optional.isPresent()) {
            optional.get().setAlias(alias);
        } else {
            if (aliasMap == null) {
                aliasMap = Maps.newHashMap();
            }
            aliasMap.put(field, alias);
        }
        return (Q) this;
    }
    
    /**
     * 为select的字段指定别名为字段本身，相当于对每一个字段调用了alias(field, field)方法
     * @return
     */
    public Q withSelectAsAlias() {
        return withSelectAsAlias(true);
    }
    
    public Q withSelectAsAlias(boolean switchOn) {
        selectAsAlias = switchOn;
        return (Q) this;
    }
    
    public boolean isSelectAsAlias() {
        return selectAsAlias;
    }
    
    @Override
    public Q fromFilterParam(AbstractFilter<?> param) {
        super.fromFilterParam(param);
        if (param instanceof AbstractQuery) {
            AbstractQuery q = (AbstractQuery) param;
            this.selects = q.selects;
            this.ignoreSelectFields = q.ignoreSelectFields;
            this.aliasMap = q.aliasMap;
            this.selectAsAlias = q.selectAsAlias;
            this.sorts = q.sorts;
            this.distinct = q.distinct;
            this.selectAll = q.selectAll;
            this.joinList = q.joinList;
            this.limit = q.limit;
            this.resultType = q.getResultType();
            this.customFirst = q.customFirst;
            this.customLast = q.customLast;
            this.unionQuery = q.unionQuery;
        }
        return (Q) this;
    }
    
    public String[] getSelectFields() {
        return selects.stream().filter(item -> item.getType() == SelectItemType.Field).map(ISelectItem::getSelect).toArray(String[]::new);
    }

    /**
     * 排除指定类字段
     * @param clazz 指定排除类，应该跟entityClass的字段名保持一致或是entityClass的父类
     * @param exceptFields 指定<strong>不</strong>排除字段
     * @return
     */
    public Q excludeClassFieldsWithExcept(Class<?> clazz, String... exceptFields) {
        return excludeSelectFields(QueryHelper.getDBFieldNames(clazz, null, exceptFields));
    }
    
    public Q excludeClassFields(Class<?> clazz) {
        return excludeSelectFields(QueryHelper.getDBFieldNames(clazz, null));
    }

    public Q excludeSelectFields(String... ignoreSelectFields) {
        this.ignoreSelectFields.addAll(Arrays.asList(ignoreSelectFields));
        return (Q) this;
    }
    
    /**
     * 排除父类的字段
     * @return
     */
    public Q excludeParentFields() {
        return excludeClassFields(super.getEntityClass().getSuperclass());
    }
    /**
     * 排除父类的字段
     * @query exceptFields 指定<strong>不</strong>排除字段
     * @return
     */
    public Q excludeParentFieldsWithExcept(String... exceptFields) {
        return excludeClassFieldsWithExcept(super.getEntityClass().getSuperclass(), exceptFields);
    }
    
    
    public Set<String> getIgnoreSelectFields() {
        return Collections.unmodifiableSet(ignoreSelectFields);
    }
    
    public boolean hasSort(String field) {
        if (sorts == null) return false;
        
        for (Sort sort : sorts) {
            if (field.equals(sort.getField())) return true;
        }
        
        return false;
    }
    
    public boolean hasAnySort() {
        if (sorts == null) return false;
        return ! sorts.isEmpty();
    }
    
    public Q sort(Sort sort) {
        prepareSorts();
        sorts.add(sort);
        return (Q) this;
    }
    
    public Q asc(String field) {
        prepareSorts();
        sorts.add(Sort.asc(field));
        return (Q) this;
    }
    
    public Q desc(String field) {
        prepareSorts();
        sorts.add(Sort.desc(field));
        return (Q) this;
    }
    
    public Q orderBy(String field, boolean asc) {
        prepareSorts();
        sorts.add(asc ? Sort.asc(field) : Sort.desc(field));
        return (Q) this;
    }
    
    public Q orderByExpression(String expression, boolean asc) {
        prepareSorts();
        sorts.add(asc ? Sort.asc(expression).withCompression() : Sort.desc(expression).withCompression());
        return (Q) this;
    }
    
    private void prepareSorts() {
        if (super.isImmutable()) throw new IllegalStateException("This instance is immutable");
        
        if(sorts == null) {
            sorts = new ArrayList<Sort>(2);
        }
    }
    
    public List<Sort> getSorts() {
        return sorts;
    }
    
    public String getOrderBy() {
        if (sorts != null && !sorts.isEmpty()) {
            StringBuilder buff = new StringBuilder();
            int index = 0;
            for (Sort sort : sorts) {
                if (sort.getField() == null) continue;
                
                String field = sort.getField();
                
                if (Strings.isEmpty(sort.getPrefix()) && ! hasPrefix(field)) {
                    String prefix = getPrefix();
                    if (prefix != null) {
                        sort.setPrefix(appendDot2Prefix(prefix));
                    }
                }
                if (index++ > 0) {
                    buff.append(", ");
                }
                if (sort.isCompression()) {
                    buff.append(MybatisHelper.getCustomSql(namespace, field, sort.getPrefix(), p -> getEntityClassByPrefix(sort.getPrefix())));
                } else {
                    buff.append(MybatisHelper.field2Column(namespace, field, sort.getPrefix(), p -> getEntityClassByPrefix(sort.getPrefix())));
                }
                if (!sort.isAsc()) {
                    buff.append(" desc");
                }
            }
            return buff.toString();
        }
        return null;
    }
    
    public List<ISelectItem> getFinalSelectFields() {
        if (!selectFieldSettled) {
            boolean hasAnySelects = this.hasAnySelects();
            
            if (!hasAnySelects) {
                if (! selectAll) {
                    throw new InvalidDataAccessResourceUsageException("Please explicit assign select fields by using method select*(...)");
                }
            }
            
            if ((!existAnySelectFields() && !ignoreSelectFields.isEmpty()) || selectAll) {
                String[] _ignoreSelectFields  = ignoreSelectFields.toArray(new String[ignoreSelectFields.size()]);
                this.selects(QueryHelper.getDBFieldNames(getEntityClass(), null, _ignoreSelectFields));
            }
            selectFieldSettled = true;
        }
        return selects;
    }
    
    public String getSelectColumn(ISelectItem selectItem) {
        String p = getPrefix() != null ? getPrefix() : "";
        if (p.length() > 0 && ! p.endsWith(".")) {
            p = p + ".";
        }
        return selectItem.getSql(p, this::getEntityClassByPrefix);
    }
    
    public String getSelectExpression(ISelectItem selectItem, Function<String, String> valueExpressionFunc) {
        return MybatisHelper.field2Expression(namespace, selectItem.getSelect(), this::getEntityClassByPrefix, valueExpressionFunc);
    }
    
    public String getSelectColumns() {
        List<ISelectItem> finalSelectFields = getFinalSelectFields();
        
        String p = getPrefix() != null ? getPrefix() : "";
        if (p.length() > 0 && ! p.endsWith(".")) p = p + ".";
        
        StringBuilder buff = new StringBuilder();
        int idx = 0;
        for (ISelectItem selectItem : finalSelectFields) {
            if (idx ++ > 0) buff.append(", ");
            buff.append(selectItem.getSql(p, this::getEntityClassByPrefix));
        }
        return buff.toString();
    }
    
    public ResultType<?> getResultType() {
        return resultType;
    }
    
    public final List<JoinQuery> getJoinList() {
        return joinList;
    }
    
    public final UnionQuery getUnionQuery() {
        return unionQuery;
    }
    
    /**
     * join(inner join)
     * @query join
     * @return
     */
    public JoinQuery<Q, EntityQuery> join(Class<?> joinEntity, String alias) {
        return doJoin(new JoinQuery(this, "join", new EntityQuery(joinEntity, alias)));
    }
    /**
     * left join
     * @query join
     * @return
     */
    public JoinQuery<Q, EntityQuery> leftJoin(Class<?> joinEntity, String alias) {
        return doJoin(new JoinQuery(this, "left join", new EntityQuery(joinEntity, alias)));
    }
    /**
     * right join
     * @query join
     * @return
     */
    public JoinQuery<Q, EntityQuery> rightJoin(Class<?> joinEntity, String alias) {
        return doJoin(new JoinQuery(this, "right join", new EntityQuery(joinEntity, alias)));
    }
    /**
     * full join
     * @query join
     * @return
     */
    public JoinQuery<Q, EntityQuery> fullJoin(Class<?> joinEntity, String alias) {
        return doJoin(new JoinQuery(this, "full join", new EntityQuery(joinEntity, alias)));
    }
    /**
     * join
     * @query join
     * @return
     */
    public <J extends AbstractQuery<?>> JoinQuery<Q, J> join(J join) {
        return doJoin(new JoinQuery(this, "join", join));
    }
    /**
     * left join
     * @query join
     * @return
     */
    public <J extends AbstractQuery<?>> JoinQuery<Q, J> leftJoin(J join) {
        return doJoin(new JoinQuery(this, "left join", join));
    }
    /**
     * right join
     * @query join
     * @return
     */
    public <J extends AbstractQuery<?>> JoinQuery<Q, J> rightJoin(J join) {
        return doJoin(new JoinQuery(this, "right join", join));
    }
    /**
     * full join
     * @query join
     * @return
     */
    public <J extends AbstractQuery<?>> JoinQuery<Q, J> fullJoinSubQuery(J join) {
        return doJoin(new JoinQuery(this, "full join", join));
    }
    
    protected <J extends AbstractQuery<?>> JoinQuery<Q, J> doJoin(JoinQuery joinQuery) {
        if (joinList == null) {
            joinList = Lists.newArrayList();
        }
        joinList.add(joinQuery);
        return joinQuery;
    }
    
    public UnionQuery union(AbstractQuery unionParam) {
        return doUnion("union", unionParam);
    }
    
    public UnionQuery unionAll(AbstractQuery unionParam) {
        return doUnion("union all", unionParam);
    }
    
    protected UnionQuery doUnion(String union, AbstractQuery query) {
        if (unionQuery == null) {
            unionQuery = new UnionQuery(namespace);
        }
        unionQuery.add(union, query);
        return unionQuery;
    }
    
    @Override
    protected Class<?> getEntityClassByPrefix(String prefix) {
        if (prefix == null) {
            return super.getEntityClassByPrefix(null);
        }
        if (joinList != null) {
            for (JoinQuery q : joinList) {
                if (prefix.equals(q.getJoinQuery().getPrefix())) {
                    return q.getJoinQuery().getEntityClass();
                }
            }
        }
        if (subQueryList != null) {
            for (SubQuery q : subQueryList) {
                if (prefix.equals(q.getQuery().getPrefix())) {
                    return q.getQuery().getEntityClass();
                }
            }
        }
        return super.getEntityClassByPrefix(prefix);
    }
    
    @Override
    public void beforeExecute() {
        super.beforeExecute();
        if (joinList != null) {
            for (JoinQuery q : joinList) {
                q.getJoinQuery().beforeExecute();
            }
        }
        if (subQueryList != null) {
            for (SubQuery q : subQueryList) {
                q.getQuery().beforeExecute();
            }
        }
        if (unionQuery != null) {
            List<UnionQuery.UnionQueryParamItem> items = unionQuery.getItems();
            if (items != null) {
                for (UnionQuery.UnionQueryParamItem item : items) {
                    if (item.getParam() != this) {
                        item.getParam().beforeExecute();
                    }
                }
            }
        }
    }
    
    protected void setSorts(List<Sort> sorts) {
        this.sorts = sorts;
    }
    
    void setResultType(ResultType<?> resultType) {
        this.resultType = resultType;
    }
    
    private boolean existAnySelectFields() {
        return !this.selects.isEmpty() && this.selects.stream().anyMatch(item -> item.getType() == SelectItemType.Field);
    }
    
    public enum SelectItemType {
        Field,
        Expression
    }
    
    public interface ISelectItem {
        String getSql(String prefix, Function<String, Class<?>> entityClassByPrefix);
        
        void setAlias(String alias);
        
        String getAlias();
        
        String getSelect();
        
        SelectItemType getType();
    }
    
    @EqualsAndHashCode
    protected class SelectField implements ISelectItem {
        private final String field;
        private String alias;
    
        public SelectField(String field) {
            this.field = field;
        }
    
        public SelectField(String field, String alias) {
            this.field = field;
            this.alias = alias;
        }
    
        @Override
        public String getSelect() {
            return field;
        }
    
        @Override
        public SelectItemType getType() {
            return SelectItemType.Field;
        }
    
        @Override
        public String getSql(String prefix, Function<String, Class<?>> entityClassByPrefix) {
            String column = MybatisHelper.field2Column(namespace, field, prefix, entityClassByPrefix);
            if (alias == null) {
                alias = isSelectAsAlias() ? field : "";
            }
            return column + (alias != null ? " as " + alias : "");
        }
    
        @Override
        public void setAlias(String alias) {
            this.alias = alias;
        }
        
        @Override
        public String getAlias() {
            return alias;
        }
    }
    
    protected class SelectExpression extends Expression implements ISelectItem {
        private String alias;
    
        public SelectExpression(String expression, Map<String, Object> argValues) {
            super(expression, argValues);
        }
    
        public SelectExpression(String expression, String alias, Map<String, Object> argValues) {
            super(expression, argValues);
            this.alias = alias;
        }
    
        @Override
        public String getSelect() {
            return expression;
        }
    
        @Override
        public SelectItemType getType() {
            return SelectItemType.Expression;
        }
    
        @Override
        public String getSql(String prefix, Function<String, Class<?>> entityClassByPrefix) {
            String select = MybatisHelper.getCustomSql(namespace, expression, getPrefix(), entityClassByPrefix);
            return select + (alias != null ? " as " + alias : "");
        }
    
        @Override
        public void setAlias(String alias) {
            this.alias = alias;
        }
        
        @Override
        public String getAlias() {
            return alias;
        }
    }
}